Day 21 最後留下了一段我以前其實會寫,但沒有真的想過的程式:
setCount(prev => prev + 1)
如果把它看久一點,事情突然很怪。
以前我以為 setCount() 裡面應該放的是:
setCount(1)
setCount(2)
setCount(3)
也就是「下一個 state 的值」。
結果現在卻可以放:
prev => prev + 1
這根本不是數字。
它是一個 function。
所以真正的問題變成:
為什麼 function 可以像數字、字串一樣,被塞進另一個 function 裡?
而且更怪的是:
我沒有自己呼叫它,React 為什麼還可以晚一點拿它來執行?
這篇先不碰 Event Loop,也不挖 React 內部實作。
我只想把一件事搞清楚:
JavaScript 裡的 function,到底是不是一種「值」?
答案是:
在 JavaScript 裡,function 本身就是一種可以被保存、傳遞、再呼叫的值。
所以這段:
prev => prev + 1
不是「現在立刻算答案」。
它是在建立一個 function value。
接著:
setCount(prev => prev + 1)
是在把這個 function 交給 React。
React 之後處理 state 更新時,再用目前該輪更新拿到的 state 去呼叫它。
所以整件事比較像:
我建立一個 function
↓
把 function 交給 setCount
↓
React 保存這個 updater
↓
React 處理 state 更新
↓
把前一個 state 傳給 updater
↓
updater 回傳新的 state
我以前只看到一行箭頭函式。
現在才開始看到:
原來這裡其實發生了「把 function 當資料傳出去」這件事。
先看:
prev => prev + 1
這段其實可以改寫成:
function addOne(prev) {
return prev + 1
}
所以:
setCount(prev => prev + 1)
也可以想成:
function addOne(prev) {
return prev + 1
}
setCount(addOne)
這樣突然清楚很多。
我不是在呼叫:
addOne()
而是把:
addOne
這個 function 本身傳進去。
這跟 Day 17 的:
<button onClick={handleClick}>
其實是同一種思路。
當時最重要的差別是:
handleClick
代表:
把 function 交出去。
而:
handleClick()
代表:
現在立刻呼叫 function,把它的回傳值交出去。
所以 Day 17 和 Day 22 其實接起來了。
先離開 React。
最普通的 JavaScript 就可以做:
const addOne = function (number) {
return number + 1
}
也可以:
const addOne = number => number + 1
這裡右邊不是先算出一個數字。
右邊就是一個 function value。
所以:
const addOne = number => number + 1
和:
const name = 'Anthony'
const count = 3
雖然值的種類不同,但「把一個值放進變數」這個動作很像。
概念上:
name
↓
字串值
count
↓
數字值
addOne
↓
function 值
這也是為什麼可以:
console.log(typeof addOne)
得到:
function
我以前會把 function 想成:
一段特別的程式碼區塊。
這沒有錯,但還不夠。
在 JavaScript 裡,它同時也是:
一個可以被參考、保存和傳遞的值。
如果 function 是值,那它就可以被當成 argument。
先做一個完全不需要 React 的例子:
function runOperation(operation, value) {
return operation(value)
}
function double(number) {
return number * 2
}
const result = runOperation(double, 5)
console.log(result)
結果:
10
一開始我可能會只看到:
runOperation(double, 5)
但把流程拆開:
double
↓
是一個 function value
↓
傳進 runOperation
↓
在 runOperation 裡變成 operation
↓
operation(value)
↓
其實就是 double(5)
↓
10
這裡最重要的是:
runOperation不需要事先知道你傳進來的一定叫double。
它只知道:
我收到一個 function,等一下我會呼叫它。
這就讓 function 變得非常有彈性。
以前看到 callback,我腦袋常會把它和非同步綁在一起。
像:
setTimeout(() => {
console.log('hello')
}, 1000)
所以很容易以為:
callback = 非同步。
其實 callback 更基本的意思是:
我把一個 function 交給另一段程式,讓對方在適合的時機呼叫它。
例如:
const numbers = [1, 2, 3]
numbers.map(number => number * 2)
這裡:
number => number * 2
也是把 function 傳給 map()。
map() 再一個一個拿 Array 裡的值呼叫它。
流程:
[1, 2, 3]
↓
map 收到一個 function
↓
function(1) → 2
function(2) → 4
function(3) → 6
↓
[2, 4, 6]
所以 Day 11 的 menuItems.map(),其實早就遇過「function 當值傳進去」了。
只是當時我的注意力都放在 Array 怎麼變成 JSX,沒有特別停下來看這件事。
setCount() 也是類似的模式現在回到:
setCount(prev => prev + 1)
可以先改寫:
function increase(prev) {
return prev + 1
}
setCount(increase)
我做的事情不是:
我先算出新的 count
↓
把數字交給 React
而是:
我把「怎麼從舊 state 算出新 state」的 function
交給 React
React 處理這個 state update 時,會把前一個 state 傳進 updater。
概念上像:
目前 pending state = 0
↓
increase(0)
↓
1
↓
下一個 updater 再拿到 1
所以 Day 21 的:
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
才能形成:
0
↓
1
↓
2
↓
3
這裡真正的關鍵不只是 React。
而是 JavaScript 先允許:
function 可以被當成值傳遞。
React 才能設計出 updater function 這種 API。
這個差異非常值得再看一次。
假設:
function addOne(number) {
return number + 1
}
addOnesomeFunction(addOne)
代表:
把 function 本身交出去。
addOne(3)someFunction(addOne(3))
則是:
先執行 addOne(3)
↓
得到 4
↓
再把 4 傳給 someFunction
所以:
addOne
↓
function value
addOne(3)
↓
呼叫 function 之後得到的結果
這跟 React:
onClick={handleClick}
和:
onClick={handleClick()}
本質上是同一個差別。
我以前會覺得只是「有沒有括號」。
現在會比較想成:
我到底是在傳一個 function,還是現在就把它執行掉?
既然 function 是值,那它不只可以被傳進去,也可以被 return 出來。
例如:
function makeMultiplier(times) {
return number => number * times
}
const double = makeMultiplier(2)
console.log(double(5))
結果:
10
先拆:
makeMultiplier(2)
↓
回傳一個 function
↓
const double = 那個 function
↓
double(5)
↓
10
這件事現在先看到就好。
因為如果繼續追:
為什麼回傳出去的 function 還記得 times = 2?
很快就會掉進另一個大洞:
Closure
這篇先不要。
但至少我開始知道為什麼 JavaScript 的 function 會一直出現在奇怪的位置。
因為它不只是「宣告完等著被呼叫」。
它本身就能在程式裡被搬來搬去。
可以直接在瀏覽器 Console 跑:
function useOperation(operation, value) {
console.log('收到的 operation:', typeof operation)
return operation(value)
}
const addTen = number => number + 10
console.log(useOperation(addTen, 5))
預期:
收到的 operation: function
15
接著故意改成:
console.log(useOperation(addTen(5), 5))
這次 addTen(5) 會先被執行,得到:
15
也就是 useOperation 收到的不再是 function,而是 number。
如果它後面還想:
operation(value)
就會出錯。
這個實驗很直接地把兩件事分開:
addTen
↓
function 本身
addTen(5)
↓
function 執行後的回傳值
待驗證: 這篇先保留成文章草稿;之後實際在 DevTools Console 跑一次,再把 observed output 補回 knowledge note。
現在回頭看幾個已經遇過的 React 寫法。
onClick<button onClick={handleClick}>
把 function 交給 React。
等使用者 click 時,React 再呼叫。
map()menuItems.map(item => <li>{item.label}</li>)
把 function 交給 map()。
map() 每拿到一個 item,就呼叫一次。
setCount() updatersetCount(prev => prev + 1)
把 function 交給 React。
React 在處理 state update 時,把前一個 state 傳進去。
看起來三個 API 完全不一樣。
但 JavaScript 底層有一個共同點:
function 可以是一個值。
突然就沒那麼像三套獨立魔法了。
不是。
這就是為什麼可以直接寫:
number => number + 1
這是一個匿名的 arrow function,也是一個 function value。
不是。
map() 的 callback 就可以同步被呼叫。
callback 描述的是:
把 function 交給別人,由對方呼叫。
至於是不是非同步,是另一個問題。
setCount(prev => prev + 1) 裡的 prev 是我自己傳進去的不是。
我提供的是 updater function。
React 在處理更新時,會用前一個 state 呼叫這個 updater。
prev 只是我幫那個參數取的名字。
所以也可以寫:
setCount(x => x + 1)
只是 prev 比較容易看懂它的角色。
() 只是語法小差異不是。
handleClick
和:
handleClick()
代表的時間點完全不同。
一個是在傳 function。
另一個是在現在就執行 function。
JavaScript 的 function 不只是可以被呼叫的程式碼,它本身也是一個值,所以可以被存進變數、當參數傳遞,也可以從其他 function 回傳。
這件事一懂,很多以前看起來很 React 的東西:
onClick={handleClick}
map(item => ...)
setCount(prev => prev + 1)
其實都可以先退回 JavaScript 來理解。
為什麼:
setCount(prev => prev + 1)
裡面可以塞 function?
因為:
prev => prev + 1
本身就是一個 function value。
我把它交給 setCount,React 之後再用前一個 state 去呼叫它。
所以不是:
setCount 只能收數字
但這裡很神奇地破例收 function
而是:
JavaScript 本來就能把 function 當值傳遞
↓
React 利用這個能力設計 updater function
↓
讓我描述「如何從前一個 state 算下一個 state」